/// attempted and we don't try the same ones again.
fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
-> CargoResult<T>
- where F: FnMut(&mut git2::Credentials) -> CargoResult<T>
+ where F: FnMut(&mut git2::Credentials) -> Result<T, git2::Error>
{
let mut cred_helper = git2::CredentialHelper::new(url);
cred_helper.config(cfg);
- let mut attempted = git2::CredentialType::empty();
- let mut failed_cred_helper = false;
-
- // We try a couple of different user names when cloning via ssh as there's a
- // few possibilities if one isn't mentioned, and these are used to keep
- // track of that.
- enum UsernameAttempt {
- Arg,
- CredHelper,
- Local,
- Git,
- }
- let mut username_attempt = UsernameAttempt::Arg;
- let mut username_attempts = Vec::new();
-
- let res = f(&mut |url, username, allowed| {
- let allowed = allowed & !attempted;
+ let mut ssh_username_requested = false;
+ let mut cred_helper_bad = None;
+ let mut ssh_agent_attempts = Vec::new();
+ let mut any_attempts = false;
+ let mut res = f(&mut |url, username, allowed| {
+ any_attempts = true;
// libgit2's "USERNAME" authentication actually means that it's just
// asking us for a username to keep going. This is currently only really
// used for SSH authentication and isn't really an authentication type.
//
// callback(SSH_KEY, user, ...)
//
- // So if we have a USERNAME request we just pass it either `username` or
- // a fallback of "git". We'll do some more principled attempts later on.
+ // So if we're being called here then we know that (a) we're using ssh
+ // authentication and (b) no username was specified in the URL that
+ // we're trying to clone. We need to guess an appropriate username here,
+ // but that may involve a few attempts. Unfortunately we can't switch
+ // usernames during one authentication session with libgit2, so to
+ // handle this we bail out of this authentication session after setting
+ // the flag `ssh_username_requested`, and then we handle this below.
if allowed.contains(git2::USERNAME) {
- attempted = attempted | git2::USERNAME;
- return git2::Cred::username(username.unwrap_or("git"))
+ debug_assert!(username.is_none());
+ ssh_username_requested = true;
+ return Err(git2::Error::from_str("gonna try usernames later"))
}
// An "SSH_KEY" authentication indicates that we need some sort of SSH
// process or from a raw in-memory SSH key. Cargo only supports using
// ssh-agent currently.
//
- // We try a few different usernames here, including:
- //
- // 1. The `username` argument, if provided. This will cover cases where
- // the user was passed in the URL, for example.
- // 2. The global credential helper's username, if any is configured
- // 3. The local account's username (if present)
- // 4. Finally, "git" as it's a common fallback (e.g. with github)
+ // If we get called with this then the only way that should be possible
+ // is if a username is specified in the URL itself (e.g. `username` is
+ // Some), hence the unwrap() here. We try custom usernames down below.
if allowed.contains(git2::SSH_KEY) {
- loop {
- let name = match username_attempt {
- UsernameAttempt::Arg => {
- username_attempt = UsernameAttempt::CredHelper;
- username.map(|s| s.to_string())
- }
- UsernameAttempt::CredHelper => {
- username_attempt = UsernameAttempt::Local;
- cred_helper.username.clone()
- }
- UsernameAttempt::Local => {
- username_attempt = UsernameAttempt::Git;
- env::var("USER").or_else(|_| env::var("USERNAME")).ok()
- }
- UsernameAttempt::Git => {
- attempted = attempted | git2::SSH_KEY;
- Some("git".to_string())
- }
- };
- if let Some(name) = name {
- let ret = git2::Cred::ssh_key_from_agent(&name);
- username_attempts.push(name);
- return ret
- }
- }
+ let username = username.unwrap();
+ debug_assert!(!ssh_username_requested);
+ ssh_agent_attempts.push(username.to_string());
+ return git2::Cred::ssh_key_from_agent(&username)
}
// Sometimes libgit2 will ask for a username/password in plaintext. This
// plaintext password is through the `credential.helper` support, so
// fetch that here.
if allowed.contains(git2::USER_PASS_PLAINTEXT) {
- attempted = attempted | git2::USER_PASS_PLAINTEXT;
let r = git2::Cred::credential_helper(cfg, url, username);
- failed_cred_helper = r.is_err();
+ cred_helper_bad = Some(r.is_err());
return r
}
// I'm... not sure what the DEFAULT kind of authentication is, but seems
// easy to support?
if allowed.contains(git2::DEFAULT) {
- attempted = attempted | git2::DEFAULT;
return git2::Cred::default()
}
Err(git2::Error::from_str("no authentication available"))
});
- if attempted.bits() == 0 || res.is_ok() {
- return res
+ // Ok, so if it looks like we're going to be doing ssh authentication, we
+ // want to try a few different usernames as one wasn't specified in the URL
+ // for us to use. In order, we'll try:
+ //
+ // * A credential helper's username for this URL, if available.
+ // * This account's username.
+ // * "git"
+ //
+ // We have to restart the authentication session each time (due to
+ // constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
+ // call our callback, `f`, in a loop here.
+ if ssh_username_requested {
+ debug_assert!(res.is_err());
+ let mut attempts = Vec::new();
+ attempts.push("git".to_string());
+ if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
+ attempts.push(s);
+ }
+ if let Some(ref s) = cred_helper.username {
+ attempts.push(s.clone());
+ }
+
+ while let Some(s) = attempts.pop() {
+ // We should get `USERNAME` first, where we just return our attempt,
+ // and then after that we should get `SSH_KEY`. If the first attempt
+ // fails we'll get called again, but we don't have another option so
+ // we bail out.
+ let mut attempts = 0;
+ res = f(&mut |_url, username, allowed| {
+ if allowed.contains(git2::USERNAME) {
+ return git2::Cred::username(&s);
+ }
+ if allowed.contains(git2::SSH_KEY) {
+ debug_assert_eq!(Some(&s[..]), username);
+ attempts += 1;
+ if attempts == 1 {
+ ssh_agent_attempts.push(s.to_string());
+ return git2::Cred::ssh_key_from_agent(&s)
+ }
+ }
+ Err(git2::Error::from_str("no authentication available"))
+ });
+
+ // If we made two attempts then that means:
+ //
+ // 1. A username was requested, we returned `s`.
+ // 2. An ssh key was requested, we returned to look up `s` in the
+ // ssh agent.
+ // 3. For whatever reason that lookup failed, so we were asked again
+ // for another mode of authentication.
+ //
+ // Essentially, if `attempts == 2` then in theory the only error was
+ // that this username failed to authenticate (e.g. no other network
+ // errors happened). Otherwise something else is funny so we bail
+ // out.
+ if attempts != 2 {
+ break
+ }
+ }
+ }
+
+ if res.is_ok() || !any_attempts {
+ return res.map_err(From::from)
}
// In the case of an authentication failure (where we tried something) then
res.chain_error(|| {
let mut msg = "failed to authenticate when downloading \
repository".to_string();
- if attempted.contains(git2::SSH_KEY) {
- let names = username_attempts.iter()
- .map(|s| format!("`{}`", s))
- .collect::<Vec<_>>()
- .join(", ");
+ if ssh_agent_attempts.len() > 0 {
+ let names = ssh_agent_attempts.iter()
+ .map(|s| format!("`{}`", s))
+ .collect::<Vec<_>>()
+ .join(", ");
msg.push_str(&format!("\nattempted ssh-agent authentication, but \
none of the usernames {} succeeded", names));
}
- if attempted.contains(git2::USER_PASS_PLAINTEXT) {
+ if let Some(failed_cred_helper) = cred_helper_bad {
if failed_cred_helper {
msg.push_str("\nattempted to find username/password via \
git's `credential.helper` support, but failed");